安安~我是ChiYu~
昨天,我把 AI Agent 的完成宣告整理成可重跑的 Evidence Packet:固定產品版本後,連同 Prompt、Diff、Build、Tests、Smoke、限制與失敗紀錄一起保存。下一位工程師因此能重新驗證,不必只相信一句「全部完成」。
但完成後能重跑,不代表開發途中能及時止損。若第一個紅燈要等三百多行修改全部寫完才出現,Reviewer 仍得在整批 Diff 裡找原因;想留下其中一部分,也未必找得到安全的回退位置。
這次我讓相同的 Codex GPT-5.6-SOL-HIGH,從同一個 Commit 處理相同的通知重試上限,只改變開發節奏:一條路線全部做完再驗證;另一條依「決策、接線、發布」拆成三個可獨立驗證與回退的週期。
結果很直接。單次大批整合第一次完整 Gate 就通過,約 7 分 3 秒、18 次 Command、103,770 個 Fresh Input Token;三個小週期則花了約 15 分 22 秒、54 次 Command、138,840 個 Fresh Input Token。兩份候選最後都完成核心行為,也都在指定檔案取得 100% Line/Branch Coverage,受控 Mutation 同樣能讓測試轉紅。
我最後接受小週期版本,不是因為它比較快,也不是因為它產生更少 Code,而是三個 Green Commit 分別固定了重試決策、Persistence 接線與發布條件。某一層失敗時,我可以只回退目前責任,不必把整批修改一起丟掉。
今天要比較的就是三件事:紅燈何時出現、失敗時要放棄多少修改,以及測試轉綠後究竟保護了哪些行為。
程式碼整理過一次,不會因此永久保持整潔。需求、依賴與使用方式持續改變,原本分得清楚的責任也會慢慢混在一起。
〈小週期〉關心每一步跨了多遠、累積多少尚未驗證的決策。它透過較小變更、短回饋週期與頻繁整合,讓風險在繼續擴大前先被看見。
我用三個條件判斷一批修改能否獨立成立。行數與 Commit 數只能協助觀察,切分單位仍是完整責任:
例如,本次的「決定何時停止重試」可以先獨立測試;「把決策接進 Dispatcher」則是下一項責任。若只是為了湊出三個 Commit,把同一段未完成的接線平均切開,仍然不算小週期。
〈持續改進〉接著追問:這一步完成後,Code 是否比原本更容易理解、驗證與修改?改善不必每次都是大型重構,也可能只是換掉模糊名稱、消除重複、補強測試,或移除已完成任務的暫時開關。
把兩個觀念放在一起,流程會形成兩層檢查:
flowchart LR
A[修改一個小批次] --> B[快速 Build 與 Tests]
B -->|Red| C[只檢查目前批次]
C --> A
B -->|Green| D[建立可回退 Commit]
D --> E{需求完成了嗎?}
E -->|還沒| A
E -->|完成| F[Coverage 找未執行路徑]
F --> G[受控 Mutation 檢查測試能否辨錯]
G --> H[語意穩定性守住既有行為]
H --> I[清理暫時結構與開關]
小週期先縮短錯誤發生到被看見的距離;功能完成後,Coverage、受控 Mutation 與語意穩定性再檢查綠燈是否可靠。只縮小批次,可能更快累積薄弱測試;所有品質檢查都留到最後,回饋又會退回大批次。
Continuous Integration(CI,持續整合)指團隊頻繁把修改整合回共享 Mainline,並由自動 Build 與 Tests 驗證每次整合。Mainline 是團隊共同整合、代表目前產品狀態的共享分支。
兩個隔離 Worktree 都沒有整合回共享 Mainline,也沒有經過團隊 CI 流程或部署到任何環境。因此,本次只能觀察縮小批次、快速驗證與保留回退位置,不能宣稱已完成持續整合、持續交付或正式部署。
昨天沒有修改產品程式碼,因此今天沿用相同的起始 Commit 與 Tag:
28758b4d9345e890e22a650d3bd7e38933982b42
day-25-harm-behavior-structure
讀者可以直接切換:
git fetch --tags
git switch --detach day-25-harm-behavior-structure
不必先下載專案,也能從這條通知路徑理解實驗為什麼適合拆成小週期。
Work Item API 會先把待傳送的通知意圖保存到 Notification Outbox。Dispatcher 掃描到 Pending 通知後才呼叫 Provider:
Sent。Pending,並安排下一次重試。目前缺少的是停止條件。Provider 若長期失敗,同一筆 Pending 通知會一直被重新排程。
這次新增的規則稱為 Retry Exhaustion(重試耗盡):當開關啟用,而且累計嘗試次數達到上限,通知便改為 Exhausted,停止自動重試,交由監控與人工處理。
stateDiagram-v2
[*] --> Pending: 建立通知意圖
Pending --> Sent: Provider 成功 ACK
Pending --> Pending: 失敗且尚未達上限
Pending --> Exhausted: 開關啟用且已達上限
Pending --> Pending: 收到 Cancellation,狀態不變並向上傳遞
完整規則如下:
| 情境 | 預期結果 |
|---|---|
EnableExhaustion = false |
維持既有重試行為,不因次數到達上限而停止 |
| 開關啟用,失敗次數尚未到上限 | 保持 Pending,安排下一次重試並清除 Lease |
開關啟用,Provider 回傳 false 且已達上限 |
保存為 Exhausted、清除 Lease,後續掃描不再呼叫 Provider |
| 開關啟用,Provider 丟出非取消例外且已達上限 | 同樣保存為 Exhausted |
| 剛好在上限那次收到成功 ACK | 保存為 Sent,不能因為到達上限就誤判失敗 |
| 收到 Cancellation | 維持既有取消語意,不得轉成一般失敗或 Exhausted |
MaximumAttempts 預設為 3,啟用新行為時必須大於零。本次只新增重試停止條件;HTTP Route、JSON、SQLite Schema、Lost ACK、冪等鍵與人工重送行為都必須維持原狀。
Feature Toggle(也常稱 Feature Flag)是透過設定切換新舊程式路徑的機制。這裡使用的是 Release Toggle:程式碼可以先整合,新行為則等監控、告警與人工處理流程準備好後再啟用。
如果直接把「無限重試」換成「達上限後停止」,而團隊還沒有處理 Exhausted 的方式,失敗通知可能安靜地停在資料庫裡。這就是本次保留舊路徑的原因。
不過,Toggle 只能切換之後要走哪條程式路徑。通知已經送出、資料已經寫入,或其他外部副作用已經發生時,把開關關掉不會自動復原。
Pete Hodgson 在 Martin Fowler 網站的 Feature Toggles 文章中提醒,Release Toggle 是過渡性機制。每多保留一條路徑,就會增加條件分支、測試組合與清理責任。因此,功能能安全拆成小增量時應優先拆分;確實需要新舊行為暫時共存時,才加入 Toggle,並同步定義移除條件。
兩個 Session 共用相同的 Repository Instruction、功能需求、起點、模型與最終驗收條件。
改變的是整套流程規約,包括何時執行測試、如何切分責任,以及何時建立 Commit。本文所稱的 Checkpoint,是一個可以獨立 Build、測試、保留與回退的狀態:
| 比較項目 | 單次大批整合 | 三個小週期 |
|---|---|---|
| 需求取得時間 | 一開始取得完整需求 | 一開始取得相同完整需求 |
| Production Code 與 Tests | 全部完成後才驗證 | 每個 Checkpoint 先建立 Red,再修到 Green |
| 完整 Gate | 所有修改完成後執行一次 | 每個 Checkpoint 都執行 |
| Commit | 最後只建立一個 | 每個獨立 Green 狀態建立一個 |
| 失敗處理 | 在整批 Diff 中修正 | 只修正或回退目前 Checkpoint |
這次比較的是兩套完整流程,不是只改一項變因。兩份 Prompt 同時改變驗證時機、Red/Green 節奏、Checkpoint 責任與 Commit 規則,而且每種流程只執行一次。因此,本文只能說明這兩次案例留下的差異,不能外推成所有模型、Repository 與團隊都會得到相同結果。
小週期沒有按行數平均切成三份:
Exhausted、Lease、例外、取消與後續掃描。本次剛好能切出三個完整責任,不代表所有需求都應拆成三段。
完整 Prompt、原始 Session、Diff 與主流程驗證都保存在 Day 27 Public Evidence。
單次整合並沒有失敗。Agent 讀懂既有 Outbox、Lease、Cancellation 與 Lost ACK,完成 Options、Policy、Dispatcher、設定與測試後,第一次執行完整 Gate 就通過 Release Build、完整測試與格式檢查,而且流程成本較低。
最後 Commit 為 83cbfdbfe81256c3c02cedf519e3c0628576339e,總 Diff 是 8 個檔案、+337/-19。
它的代價出現在回退範圍:Options、Policy、Dispatcher、Persistence、設定與測試必須一起保留或一起放棄。Reviewer 無法先接受重試決策,再單獨退回資料庫接線。
讀者可以查看單次大批整合的完整 Diff。
小週期流程留下三個可以獨立回查的階段:
| Checkpoint | 紅燈或驗證訊號 | 它隔離的問題 | Green Commit |
|---|---|---|---|
| 重試決策 | 測試先因型別與決策尚未存在而編譯失敗;加入新簽章後,既有 Dispatcher 呼叫端再出現編譯錯誤 | 純決策是否成立,以及新簽章如何與舊呼叫端相容 | 985382ab... |
| Dispatcher 與 Persistence | 預期後續掃描不再通知,實際仍呼叫 Provider 一次 | Policy 已能回答 MarkExhausted,Dispatcher 卻尚未使用這個決策 |
8be89e3... |
| 發布與回歸條件 | Toggle、成功 ACK、Options 驗證與既有行為全部通過 | 新行為能否安全關閉、啟用與清理 | f58ab96... |
第一個 Checkpoint 的初始 Red,是先寫測試後預期出現的規格缺口,不是 Agent 意外犯錯。接著出現的編譯錯誤,才揭露新方法簽章會影響既有 Dispatcher。
為了讓純重試決策先獨立成立,Agent 暫時保留相容多載,資料庫接線則留到下一個 Checkpoint 完成。
第二個 Checkpoint 的行為 Red 同樣是刻意建立的:
Expected Provider calls after second scan: 0
Actual Provider calls after second scan: 1
測試預期第二次掃描不再呼叫 Provider,實際卻又呼叫一次。這表示 Policy 已經會判斷停止重試,但 Dispatcher 尚未把決策保存到資料庫。
完成接線後,失敗通知會保存為 Exhausted、清除 Lease,後續掃描不再取得它;非取消例外與 Cancellation 也維持各自語意。
第三個 Checkpoint 不再修改主要 Dispatcher 流程,專心補齊發布條件:預設關閉時維持舊路徑;剛好達到上限時,只要 ACK 成功,狀態仍是 Sent;錯誤設定則在啟動時被擋下,並把預設值保存到正式設定。
三個 Checkpoint 合計 9 個檔案、+343/-26,與大批整合的 Code 量接近。因此,小週期的差異不在產碼變少,而在紅燈出現的位置與可回退的責任範圍。
讀者可以查看三個小週期的完整 Diff 與 Commit。

圖:小週期用額外測試成本換取更早回饋與較小回退範圍,但前提仍是測試本身值得信任。
兩位 Agent 都替自己的實作寫了測試,但測試案例的拆法不同。有人使用 Theory,也就是以同一個測試方法驗證多組輸入;另一份候選則拆成多個具名測試。兩種寫法會產生不同測試數量,因此不能直接拿總數比較品質。
所以我另外固定三項 Host Oracle。Oracle 是 User 在看到候選前先決定的預期答案,不由 Agent 依自己的實作補寫。
| 事前固定的核心行為 | 單次大批整合 | 三個小週期 |
|---|---|---|
開關啟用,第一次失敗即達上限時,保存為 Exhausted,後續不再呼叫 Provider |
通過 | 通過 |
上限那次收到成功 ACK,狀態仍為 Sent |
通過 | 通過 |
開關關閉時,達上限後仍維持 Pending |
通過 | 通過 |
主流程也確認兩份候選都能完成 Release Build、完整測試與格式檢查。HTTP Smoke 使用兩筆固定的到期測試資料:首次執行時,系統會處理它們並嘗試通知;對相同資料再次執行時,則不會重複處理或通知。
到這裡,只能判定兩份候選的核心功能都成立。要不要接受小週期版本,還要比較回退範圍、流程成本與最後的程式結構。
功能完成後,我用三層檢查處理〈持續改進〉關心的問題。
Semantic Stability(語意穩定性)要求的是:程式真正的意義被改錯時,測試應該失敗;只整理內部結構時,外部可觀察行為則維持原樣。
| 檢查 | 回答的問題 | 本次結果 |
|---|---|---|
| Coverage | 新增路徑有沒有被測試執行? | 兩份候選的 Options、Policy 與 Dispatcher 都得到 100% Line/Branch Coverage |
| 受控 Mutation | 條件被改錯時,測試會不會失敗? | 等號邊界與 Toggle 反向都讓兩份候選轉紅 |
| Semantic Stability | 新規則有沒有順便改掉既有意義? | 受保護檔案內容相同,固定 Oracle 與 HTTP Smoke 也都通過 |
兩份候選的 NotificationRetryOptions、NotificationDispatchPolicy 與 EfCoreNotificationOutboxDispatcher 都得到 100% Line/Branch Coverage。
這項結果只能說新設定、判斷與 Persistence 路徑有被測試走過。測試即使執行每一行,Assert 仍可能沒有檢查正確答案。
>= 改成 >、再反轉功能開關,確認測試真的會報錯我接著對兩份候選各放入兩項受控錯誤。
第一項破壞剛好等於上限的邊界:
- attemptCount >= maximumAttempts
+ attemptCount > maximumAttempts
第二項把 EnableExhaustion 的開關判斷反向。
兩項錯誤都讓相關測試轉紅;還原正確 Code 後,測試重新通過。這比只看到 Coverage 100% 多回答一個問題:測試不只走過程式路徑,至少也能辨認今天最在意的邊界與開關錯誤。
這仍然不是完整的 Mutation Score。我只挑出兩個與需求直接相關的高風險判斷,沒有宣稱所有可能錯誤都會被測試抓到。
Git 會依檔案內容建立 Blob 物件。相同的 Git Blob SHA 表示檔案內容逐位元相同,不代表整個系統的執行結果必然相同。
起點與兩份候選的 Controller、公開 Contracts、DbContext、既有 Models 與人工重送 Scheduler 都維持相同 Blob SHA。這能支持「這些檔案沒有被順便改寫」。
不過,設定、依賴注入與間接呼叫仍可能改變執行結果,因此我還是重跑固定 Oracle、完整測試與 HTTP Smoke,確認 API 回應、資料 Mapping 與重複通知防線沒有漂移。
兩條 Session 最容易解讀的成本如下:
| 指標 | 單次大批整合 | 三個小週期 |
|---|---|---|
| 執行時間 | 約 7 分 3 秒 | 約 15 分 22 秒 |
| Command Tool Call | 18 | 54 |
| Fresh Input Token | 103,770 | 138,840 |
Command Tool Call 是 Agent 呼叫命令列工具的次數。Fresh Input Token 沿用前文定義,表示扣除快取後重新讀入的內容量。
結果很直接:小週期這次更慢,也使用更多 Token。每一輪都要重新確認狀態、執行 Gate、檢查 Diff 並建立 Commit,這些都是為了縮小回退範圍而支付的成本。
這些數字也包含兩份 Prompt 刻意規定的不同流程,而且每種流程只有一次 Run。它們只能描述本次成本,不能拿來當模型的通用效能排名。
如果只是修改局部純函式、文件或內部 Rename,完整 Gate 幾秒內就能完成,一次 Revert 也不會丟掉其他已成立的決策,硬拆三個 Checkpoint 反而會浪費 Agent 的推理與工具成本。
通知重試上限同時碰到外部 Provider、持久化狀態、例外、取消與漸進啟用。這種需求,我願意支付額外時間與 Token,換取三個已知可用的 Green Commit,以及更精準的紅燈範圍。
流程上,我接受三個小週期版本。原因是這項需求可以切出三個完整責任,而且任一層出問題時,都有清楚的上一個 Green Commit。
程式結構仍要另外判斷。兩份候選對重試設定採取不同做法:
| 候選 | DecideCompletion 如何取得重試設定 |
適合情境 |
|---|---|---|
| 單次大批整合 | 呼叫端每次傳入 enableExhaustion 與 maximumAttempts |
門檻會依租戶、訊息種類或每次呼叫改變,需要讓每項輸入明確出現在方法簽章 |
| 三個小週期 | NotificationDispatchPolicy 建立時取得固定 NotificationRetryOptions,Dispatcher 只傳 acknowledged 與 attemptCount |
整個 Host 共用同一組重試規則,希望由 Policy 集中擁有設定與決策 |
目前整個 API Host 共用同一組重試設定,因此我選擇由 Policy 持有 Retry Options。Dispatcher 只需要提供本次通知結果與累計次數,不必同時理解設定來源、功能開關與重試上限。
大批候選的顯式參數不是壞設計。未來若重試上限需要依租戶或訊息動態改變,我反而會重新考慮它。Clean Code 沒有要求所有情境只能長成同一個方法簽章。
本次接受 Commit 為 f58ab96bf3552ad584061eec70757a84848963b5,Annotated Tag 為 day-27-small-cycles-improvement。
讀者可以直接切換:
git fetch --tags
git switch --detach day-27-small-cycles-improvement
L — Localized Change 局部變更 在這篇檢查兩件事:最終 Diff 有沒有超出需求,以及兩次 Green 之間累積多少尚未驗證的決策。
只看檔案數會漏掉差異:單次整合只修改 8 個檔案,甚至比小週期少一個,但第一次完整 Gate 前已經累積六種責任。
大批整合在第一次完整 Gate 前,同時累積 Options、Policy、Dispatcher、Persistence、設定與所有測試;小週期把每次尚未驗證的範圍限制在一項可以命名的責任。
L 要我在交付任務前先回答:「這一輪若失敗,我需要放棄多少已完成的決策?」
昨天的 Evidence Packet 保存最終完成結果。今天的 A — Auditable by Evidence 實據可審 把回查範圍往前延伸到每個 Checkpoint。
我需要知道紅燈來自規格尚未實作、編譯整合、行為 Assertion,還是工具環境;Green 又實際執行哪些 Gate。缺少這些資料,三個 Commit 可能只是把同一批修改分三次存檔,稱不上小週期。
這次保留三個 Commit、每段 Diff、紅燈原因與驗證結果,再由主流程補上相同 Oracle、Coverage、受控 Mutation 與語意穩定性檢查。Reviewer 才能確認我所說的「回饋更早、過程較容易回查」是否真的成立。
以下先用 AGENTS.md 格式整理可重複使用的判斷,尚未寫入 API Demo。未來抽成專門 Skill 會更適合:
## Small-Cycle and Continuous-Improvement Policy
- 先依變更風險、Gate 速度與可回退範圍選擇整合節奏,
不用 Commit、檔案或行數判定品質。
- 低風險、局部、可由快速 Gate 一次驗收,而且能整批安全放棄的修改,
可以採單次整合;完成後仍須回報停止理由與回退範圍。
- 涉及 Schema、權限、金流、並行、持久化狀態轉換或外部副作用時,
優先拆成能獨立 Build、Test、Commit 與 Revert 的 Checkpoint。
- 每個 Checkpoint 必須完成一項可命名的決策,禁止按行數平均切割。
- 進入下一個 Checkpoint 前,保存目前的 Red 原因、Green Gate、Diff、
Commit 與未驗證範圍;不得把失敗留給下一段。
- Feature Toggle 只在新舊行為必須暫時共存時使用;加入時同步定義
Toggle Off/On 測試、Owner、啟用條件與移除條件。
- 功能 Gate 通過後,針對本次新增決策檢查 Coverage 缺口;
高 Coverage 不得直接當成測試品質證明。
- 對高風險比較、Boolean、狀態轉換或副作用順序,執行受控 Mutation,
或明列未執行原因與替代驗證。
- HTTP Contract、Database Mapping、錯誤語意與既有副作用順序,
列入 Semantic Stability 清單並由測試或 Smoke 保護。
- Toggle、相容層與暫時分支完成任務後,建立獨立清理需求,
不得永久留在 Repository。
這份 Policy 不會把每項需求固定切成三段。Agent 可以依風險選擇一次完成、兩個 Checkpoint 或更多小週期;我主要審查切分理由、每段 Gate 與回退邊界,實作細節仍交給 Agent 自主決定。
回到標題,一次改完再驗證的版本在本次實驗中比較快、工具呼叫較少,也第一次就通過完整 Gate;三個小週期則花更多時間與 Token。小週期並沒有免費提高效率。
我仍選擇小週期,是因為這項高風險需求能切成三個完整責任,而且每一段都有可辨認的 Red、Green 與回退位置。若需求只是局部 Rename、文件或快速純函式修改,我會保留一次完成,不為流程形式額外拆 Commit。
Clean Code 提供可理解、可測與可修改的品質目標;小週期控制每一步累積多少風險;持續改進則避免測試全綠後就停止檢查。L — Localized Change 局部變更 限制兩次綠燈之間能累積多少決策;A — Auditable by Evidence 實據可審 則要求每個階段留下足以回查的紅燈原因與驗證結果。
Feature Toggle 也需要明確終點。當所有目標環境都已啟用新規則、Exhausted 已有監控與人工處理方式、觀察期結束,而且人工重送規則確認穩定,就應移除 Toggle 與舊路徑。
明天,我會把這套個人工作節奏放進多人與多 Agent 協作。產碼變快之後,Review、整合與接手成本是否也跟著下降,會是下一個要驗證的問題。